跳到主要内容

HTTP 2.0 学习

历代 HTTP 对比

HTTP 1.0 的时候一个请求一个 TCP 连接

HTTP 1.1 使用 KeepAlive 允许复用一个 TCP 连接,可以看下图,多次请求实际上都在同一个连接中,一般浏览器是可以打开多个连接进行并行请求资源的(不过浏览器最多允许 6 个,因为过多的连接就变成 DOS 攻击了)

HTTP 2.0 使用二进制分帧层,以及头部压缩来提升响应速度

HTTP/2 允许在单个连接上同时执行多个请求,每个 HTTP 请求或响应使用不同的流。通过使用二进制分帧层,给每个帧分配一个流标识符,以支持同时发出多个独立请求。当接收到该流的所有帧时,接收方可以将帧组合成完整消息。

总结 HTTP2 最大的改进

HTTP/2相对于HTTP/1.1引入了许多重要的改进,其中最显著的改进包括以下几点:

  1. 多路复用(Multiplexing):HTTP/2 通过引入多路复用机制,允许在单个 TCP 连接上同时发送多个请求和响应。这意味着不再需要为每个请求都建立新的连接,从而减少了延迟和连接建立的开销,并提高了性能和效率。

  2. 二进制分帧(Binary Framing):HTTP/2 将传输的数据分割为更小的二进制帧(frames),每个帧都包含有关该帧的信息,并可以独立发送和处理。这种二进制分帧的机制使得数据传输更加高效、灵活,并且可以更好地处理优先级和流控制。

  3. 头部压缩(Header Compression):HTTP/2 使用了 HPACK 算法对头部信息进行压缩,减少了重复头部的传输量。这样可以节省带宽,并减少了延迟。头部压缩还可以避免了一些安全问题,例如HTTP劫持(HTTP Header Injection)攻击。

  4. 服务器推送(Server Push):HTTP/2允许服务器在客户端请求之前主动推送相关资源。服务器可以通过解析HTML文档或其他资源,预测客户端可能需要的资源,并将这些资源主动推送给客户端,减少了客户端的请求次数,加快了页面加载速度。

  5. 流量控制(Flow Control):HTTP/2引入了流量控制机制,允许发送方和接收方通过动态调整数据流的速率,以适应不同的网络条件和接收方的处理能力。这样可以避免过载和拥塞,并确保更好的性能和可靠性。

这些改进使得HTTP/2相对于HTTP/1.1具有更好的性能、效率和安全性,能够更好地满足现代Web应用对速度和可扩展性的要求。

HTTP 2.0 的头部压缩是什么?

在传统的 HTTP/1.1 协议中,每次请求或响应都需要携带大量的头部信息,包括请求行、请求头、响应行、响应头等。这些头部信息可能包含重复的内容,比如 Cookie、User-Agent 等,导致每次请求都需要重复传输相同的数据,造成了带宽的浪费。

而在 HTTP/2.0 中,引入了一种称为 HPACK 的头部压缩算法,它可以在客户端和服务器之间对头部信息进行压缩和解压缩。HPACK 采用了基于字典的压缩方法,使用静态字典和动态字典来管理头部字段,并使用索引表来进一步减少数据的传输量。

具体来说,HPACK 的工作原理如下:

  1. 客户端和服务器都维护一个动态表,用于存储最近使用的头部字段。
  2. 在通信开始时,客户端和服务器都有一个初始的静态表,其中包含了一些常见的 HTTP 头部字段,比如方法、状态码等。
  3. 当需要传输头部信息时,客户端和服务器可以通过索引表、静态表和动态表来寻找和表示已知的头部字段。
  4. 头部字段可以使用索引号来表示,从而避免传输具体的字段名和字段值。
  5. 如果头部字段不在索引表、静态表和动态表中,那么客户端或服务器可以发送一个新的头部字段,同时将其添加到动态表中,以便后续的引用。

通过使用 HPACK 头部压缩,HTTP/2.0 可以显著减少传输的头部数据量,从而降低延迟并提高性能。同时,压缩和解压缩的过程也会引入一些开销,但由于头部数据通常较小,所以整体效果还是非常显著的。

HTTP2 与 HTTP1.1 的管道化区别

图中第一种请求方式,就是单次发送 request 请求,收到 response 后再进行下一次请求,显然是很低效的。

于是 http1.1 提出了管线化(pipelining)技术,就是如图中第二中请求方式,一次性发送多个 request 请求。

然而 pipelining 在接收 response 返回时,也必须依顺序接收,如果前一个请求遇到了阻塞,后面的请求即使已经处理完毕了,仍然需要等待阻塞的请求处理完毕。这种情况就如图中第三种,第一个请求阻塞后,后面的请求都需要等待,这也就是 队头阻塞(Head of line blocking)。

为了解决上述阻塞问题,http2 中提出了多路复用(Multiplexing)技术,Multiplexing 是通信和计算机网络领域的专业名词。http2 中将多个请求复用同一个 tcp 链接中,将一个 TCP 连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。也就是将每个 request-response 拆分为了细小的二进制帧 Frame,这样即使一个请求被阻塞了,也不会影响其他请求,如图中第四种情况所示。

总结

HTTP/1.1的管道化(Pipeline)和HTTP/2的多路复用(Multiplexing)是两种不同的技术机制,用于在单个连接上发送多个请求。

HTTP/1.1管道化允许客户端在一个TCP连接上按顺序发送多个请求,而无需等待每个请求的响应。这样可以减少请求的延迟,并提高性能。然而,管道化在实际应用中并不广泛使用,主要是因为它存在以下问题:

  1. 污染问题(Head-of-line Blocking):如果管道化中的某个请求出现延迟或阻塞,后续的请求必须等待该请求的响应,无法继续进行处理,从而导致了所谓的"头部阻塞"问题。这会降低整体的效率和性能。

  2. 顺序依赖性:由于HTTP/1.1管道化要求响应必须按照请求的顺序返回,因此如果前面的请求响应较慢,会导致后面的请求被阻塞。这种顺序依赖性限制了并发处理的能力。

相比之下,HTTP/2采用了多路复用的机制。它允许在单个TCP连接上同时发送多个请求和响应,并且没有顺序依赖性。这样可以避免了HTTP/1.1中的头部阻塞问题,并提高了性能和效率。

在HTTP/2中,每个请求和响应都被划分为一个或多个帧(frames),每个帧都有自己的标识符,并且可以独立发送和处理。这意味着可以在同一个连接上并行发送和接收多个请求和响应,不需要等待前一个请求的响应。这样可以充分利用带宽,提高并发性能,减少延迟。

总而言之,HTTP/2的多路复用机制相比HTTP/1.1的管道化更为先进和高效,能够更好地满足现代Web应用的需求。

如何创建一个 HTTP2

HTTP/2 和 HTTP/1 在连接层的巨大差异,客户端Web浏览器和服务端都需要支持 HTTP/2,才可以正常使用。由于涉及互相独立的两端,所以需要有一个过程来确认两端都想使用、都可以使用 HTTP/2。

  • 使用 HTTPS 协商。
  • 使用 HTTP Upgrade 首部。
  • 和之前的连接保持一致。
  • 使用 HTTP Alternative Services
提示

理论上,HTTP/2 支持基于未加密的 HTTP(也就是h2c)创建连接,也支持基于加密的 HTTPS(即h2)创建连接。实际上,所有的 Web 浏览器仅支持基于 HTTPS(h2)建立 HTTP/2 连接,所以浏览器使用第一个方法来协商 HTTP/2。服务器之间的 HTTP/2 连接可以基于未加密的 HTTP(h2c)或者 HTTPS(h2)。

通过 HTTPS 的方式

由于当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2,因为浏览器是基于 ALPN 协议来判断服务器是否支持 HTTP2 协议。

浏览器在进行 SSL 连接,第一次发送 ClientHello 包时,用 SSL 的扩展字段,携带浏览器支持的版本,其中 h2 代表浏览器支持 http2 协议。

服务器在返回 Server Hello 包时,如果服务器支持 H2 协议,则会返回 H2,如果不支持,那么客户端的协议列表中选取一个它支持的协议。

接下来就可以以 http2 的方式发包了,这也是为什么 Nginx 配置了 http2 以后,还需要升级 OpenSSL 到 1.0.2,因为 Nginx 的 SSL 连接用的是 OpenSSL 这个库,这个库在 Centos6.8 上用的是 1.0.1 这个版本,这个版本不支持 H2,所以在 SSL 秘钥协商时就不会返回支持 H2 的标识,所以还是用 http1.1

使用 HTTP Upgrade 首部

通过发送 Upgrade 首部,客户端可以请求将现有的 HTTP/1.1 连接升级为 HTTP/2。这个首部应该只用于未加密的 HTTP 连接(h2c)。

如下创建一个 h2c 服务端,它支持 golang 本身支持的标准 HTTP/2和 HTTP/1.1

import (
"log"
"net"
"net/http"

"golang.org/x/net/http2/h2c"
"golang.org/x/net/http2"
)

func main() {
lsrv, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}

srv := http.Server{
Handler: h2c.NewHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
log.Println(r)
rw.Write([]byte("hello world"))
}), &http2.Server{}),
}

if err := http2.ConfigureServer(&srv, &http2.Server{}); err != nil {
panic(err)
}

if err := srv.Serve(lsrv); err != nil {
panic(err)
}
}

这里使用 curl 工具发送 http2 请求

$ curl -v --http2 http://localhost:8080

检查控制台打印:

* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 11
< date: Tue, 25 Jan 2022 03:29:17 GMT
<
* Connection #0 to host localhost left intact
hello world%

可以发现是通过 HTTP/1.1连接,然后升级到 HTTP/2(H2C)

这里主要介绍下使用这种方式的缺点

在 Client 和 Server 中间可能存在并不支持 http2 的代理服务器,对于 Server 回复的 Upgrade 首部,代理会转发,然而他自己并不支持

这也是为什么所有浏览器不支持 h2c 升级为 http2 的原因

Reference